/*
 * Copyright (C) 2014 Alpha, Inc.
 * This is NOT a freeware, use is subject to license terms.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cc.gehua.octopus.rpc.common.resource;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author guna
 */
class JarFileResource extends JarResource {

    private static final Logger LOG = LoggerFactory.getLogger(JarFileResource.class);
    private JarFile jarFile;
    private File file;
    private String[] list;
    private JarEntry ResEntry;
    private boolean ResDirectory;
    private String jarFileUrl;
    private String filePath;
    private boolean fileExists;

    /* -------------------------------------------------------- */
    protected JarFileResource(URL url) {
        super(url);
    }

    /* ------------------------------------------------------------ */
    protected JarFileResource(URL url, boolean useCaches) {
        super(url, useCaches);
    }


    /* ------------------------------------------------------------ */
    @Override
    public synchronized void close() {
        list = null;
        ResEntry = null;
        file = null;
        //if the jvm is not doing url caching, then the JarFiles will not be cached either,
        //and so they are safe to close
        if (!getUseCaches()) {
            if (jarFile != null) {
                try {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Closing JarFile " + jarFile.getName());
                    }
                    jarFile.close();
                } catch (IOException ioe) {

                }
            }
        }
        jarFile = null;
        super.close();
    }

    /* ------------------------------------------------------------ */
    @Override
    protected synchronized boolean checkConnection() {
        try {
            super.checkConnection();
        } finally {
            if (jarConnection == null) {
                ResEntry = null;
                file = null;
                jarFile = null;
                list = null;
            }
        }
        return jarFile != null;
    }


    /* ------------------------------------------------------------ */
    @Override
    protected synchronized void newConnection()
            throws IOException {
        super.newConnection();

        ResEntry = null;
        file = null;
        jarFile = null;
        list = null;

        int sep = urlString.indexOf("!/");
        jarFileUrl = urlString.substring(0, sep + 2);
        filePath = urlString.substring(sep + 2);
        if (filePath.length() == 0) {
            filePath = null;
        }
        jarFile = jarConnection.getJarFile();
        file = new File(jarFile.getName());
    }

    /* ------------------------------------------------------------ */
    /**
     * Returns true if the represented resource exists.
     */
    @Override
    public boolean exists() {
        if (fileExists) {
            return true;
        }

        if (urlString.endsWith("!/")) {

            String file_url = urlString.substring(4, urlString.length() - 2);
            try {
                return newResource(file_url).exists();
            } catch (Exception e) {
                return false;
            }
        }

        boolean check = checkConnection();

        // Is this a root URL?
        if (jarFileUrl != null && filePath == null) {
            // Then if it exists it is a directory
            ResDirectory = check;
            return true;
        } else {
            // Can we find a file for it?
            JarFile iJarFile = null;
            if (check) // Yes
            {
                iJarFile = this.jarFile;
            } else {
                // No - so lets look if the root entry exists.
                try {
                    JarURLConnection c = (JarURLConnection) ((new URL(jarFileUrl)).openConnection());
                    c.setUseCaches(getUseCaches());
                    iJarFile = c.getJarFile();
                } catch (Exception e) {

                }
            }

            // Do we need to look more closely?
            if (iJarFile != null && ResEntry == null && !ResDirectory) {
                // OK - we have a JarFile, lets look at the entries for our path
                Enumeration<JarEntry> e = iJarFile.entries();
                while (e.hasMoreElements()) {
                    JarEntry entry = e.nextElement();
                    String name = entry.getName().replace('\\', '/');

                    // Do we have a match
                    if (name.equals(filePath)) {
                        ResEntry = entry;
                        // Is the match a directory
                        ResDirectory = filePath.endsWith("/");
                        break;
                    } else if (filePath.endsWith("/")) {
                        if (name.startsWith(filePath)) {
                            ResDirectory = true;
                            break;
                        }
                    } else if (name.startsWith(filePath) && name.length() > filePath.length() && name.charAt(filePath.length()) == '/') {
                        ResDirectory = true;
                        break;
                    }
                }
            }
        }

        fileExists = (ResDirectory || ResEntry != null);
        return fileExists;
    }

    /* ------------------------------------------------------------ */
    /**
     * Returns true if the represented resource is a container/directory. If the
     * resource is not a file, resources ending with "/" are considered
     * directories.
     */
    @Override
    public boolean isDirectory() {
        return urlString.endsWith("/") || exists() && ResDirectory;
    }

    /* ------------------------------------------------------------ */
    /**
     * Returns the last modified time
     */
    @Override
    public long lastModified() {
        if (checkConnection() && file != null) {
            if (exists() && ResEntry != null) {
                return ResEntry.getTime();
            }
            return file.lastModified();
        }
        return -1;
    }

    /* ------------------------------------------------------------ */
    @Override
    public synchronized String[] list() {
        if (isDirectory() && list == null) {
            List<String> iListist;
            try {
                iListist = listEntries();
            } catch (Exception e) {
                //Sun's JarURLConnection impl for jar: protocol will close a JarFile in its connect() method if
                //useCaches == false (eg someone called URLConnection with defaultUseCaches==true).
                //As their sun.net.www.protocol.jar package caches JarFiles and/or connections, we can wind up in 
                //the situation where the JarFile we have remembered in our iJarFile member has actually been closed
                //by other code.
                //So, do one retry to drop a connection and get a fresh JarFile
                LOG.warn("Retrying list:", e);
                close();
                iListist = listEntries();
            }

            if (iListist != null) {
                this.list = new String[iListist.size()];
                iListist.toArray(this.list);
            }
        }
        return list;
    }

    /* ------------------------------------------------------------ */
    private List<String> listEntries() {
        checkConnection();

        ArrayList<String> listA = new ArrayList<>(32);
        JarFile ijarFile = this.jarFile;
        if (ijarFile == null) {
            try {
                JarURLConnection jc = (JarURLConnection) ((new URL(jarFileUrl)).openConnection());
                jc.setUseCaches(getUseCaches());
                ijarFile = jc.getJarFile();
            } catch (IOException e) {

                LOG.warn("exception",e);
            }
            if (ijarFile == null) {
                throw new IllegalStateException();
            }
        }

        Enumeration<JarEntry> e = ijarFile.entries();
        String dir = urlString.substring(urlString.indexOf("!/") + 2);
        while (e.hasMoreElements()) {
            JarEntry entry = e.nextElement();
            String name = entry.getName().replace('\\', '/');
            if (!name.startsWith(dir) || name.length() == dir.length()) {
                continue;
            }
            String listName = name.substring(dir.length());
            int dash = listName.indexOf('/');
            if (dash >= 0) {
                //when listing jar:file urls, you get back one
                //entry for the dir itself, which we ignore
                if (dash == 0 && listName.length() == 1) {
                    continue;
                }
                //when listing jar:file urls, all files and
                //subdirs have a leading /, which we remove
                if (dash == 0) {
                    listName = listName.substring(dash + 1, listName.length());
                } else {
                    listName = listName.substring(0, dash + 1);
                }

                if (listA.contains(listName)) {
                    continue;
                }
            }

            listA.add(listName);
        }

        return listA;
    }

    /* ------------------------------------------------------------ */
    /**
     * Return the length of the resource
     */
    @Override
    public long length() {
        if (isDirectory()) {
            return -1;
        }

        if (ResEntry != null) {
            return ResEntry.getSize();
        }

        return -1;
    }

    /**
     * Take a Resource that possibly might use URLConnection caching and turn it
     * into one that doesn't.
     *
     * @param resource
     * @return the non-caching resource
     */
    public static Resource getNonCachingResource(Resource resource) {
        if (!(resource instanceof JarFileResource)) {
            return resource;
        }

        JarFileResource oldResource = (JarFileResource) resource;

        JarFileResource newResource = new JarFileResource(oldResource.getURL(), false);
        return newResource;

    }

    /**
     * Check if this jar:file: resource is contained in the named resource. Eg
     * <code>jar:file:///a/b/c/foo.jar!/x.html</code> isContainedIn
     * <code>file:///a/b/c/foo.jar</code>
     *
     * @param resource
     * @return true if resource is contained in the named resource
     * @throws MalformedURLException
     */
    @Override
    public boolean isContainedIn(Resource resource)
            throws MalformedURLException {
        String string = urlString;
        int index = string.indexOf("!/");
        if (index > 0) {
            string = string.substring(0, index);
        }
        if (string.startsWith("jar:")) {
            string = string.substring(4);
        }
        URL sUrl = new URL(string);
        return sUrl.sameFile(resource.getURL());
    }
}
